iT邦幫忙

2022 iThome 鐵人賽

DAY 13
1
Modern Web

終究都要學 React 何不現在學呢?系列 第 13

終究都要學 React 何不現在學呢? - React 進階 - useMemo - (13)

  • 分享至 

  • xImage
  •  

前言

這一章節開始我們要來介紹一個新的 Hook 也就是 useMemo,但是再說明 useMemo 之前我們先來回顧一下 Vue 裡面與 useMemo 類似的東西,也就是 Vue Computed。

Vue Computed

首先讓我們先回憶一下關於 Vue Computed 的用法,我們先來看一下下面這個例子:

const { createApp, ref, computed } = Vue;

const app = createApp({
  setup() {
    const num1 = ref(2);
    const total = computed(() => num1.value * 2)
    
    return {
      total
    }
  }
});

app.mount('#app');

CodePen 連結

在上方的例子中我們可以看到我們所熟悉的 Computed (計算屬性),而這個計算屬性會回傳 num1 乘以 2 的值,而這就是我們所熟悉的 Composition API Computed 計算屬性最基本的用法。

忘了 Computed 的用法嗎?沒關係,這邊寫了一個簡單的範例讓你回憶一下

const { createApp, ref, computed } = Vue;

const app = createApp({
  setup() {
    const num1 = ref(2);
    const total = computed(() => {
      console.log('computed');
      return num1.value * 2
    })
    
    const add = () => {
      console.log('methods');
      return num1.value * 2
    }
    
    return {
      total,
      add
    }
  }
});

app.mount('#app');

CodePen 連結

當你打開 Console 的時候,你可以發現 Add 函式總共被呼叫了五次,因此 console.log('methods') 也出現五次,而這是因為我們在畫面上呼叫它了五次,但是 Computed 明明也是呼叫五次,但 console.log('computed') 卻只出現一次,這是因為 Computed 只會在資料改變時才會重新計算並執行,因此當資料沒有任何變化時,就不會再次執行,而這也就是我們所熟悉的 Computed 的特性。

那麼 React 的 useMemo 又是怎樣呢?讓我們接著往下看吧。

useMemo

看到前面的例子後,我們可以知道 Vue 的 Computed 會在資料有變動時才會重新計算,而這個特性就是我們在 React Hook 中的 useMemo

首先讓我們將前面 Vue 的範例稍微改成 React Hook 版本,看一下 useMemo 是不是真的與 Vue Computed 類似

const App = () => {
  const num = 1;
  
  
  const total = React.useMemo(() => {
    console.log('useMemo')
    return num * 2
  });
  
  const add = () => {
    console.log('methods');
    return num * 2;
  }
  
  return (
    <div>
      <p>useMemo:{ total }</p>
      <p>useMemo:{ total }</p>
      <p>useMemo:{ total }</p>
      <p>useMemo:{ total }</p>
      <p>useMemo:{ total }</p>
      
      <p>methods:{ add() }</p>
      <p>methods:{ add() }</p>
      <p>methods:{ add() }</p>
      <p>methods:{ add() }</p>
      <p>methods:{ add() }</p>
    </div>
  )
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

以結果來講,確實是一樣的,因此我們可以知道 useMemo 也是在資料有變動時才會重新計算,而這也是我們在 React Hook 中的 useMemo 的特性。

所以我們就到這邊結束囉~

.
..
...
....
.....

沒有啦,其實 useMemo 還有其他特性,所以讓我們繼續往下看吧。

首先先讓我們看一段範例程式碼

const App = () => {
  const [ users, setUsers ] = React.useState([]);
  
  const getData = async () => {
    // 有時候會發生 CORS,只需要重新整理即可
    const { data } = await axios.get('https://randomuser.me/api/?results=10');
    setUsers(data.results);
  }
  
  React.useEffect(() => {
    getData()
  }, []);
  
  return (
    <div>
      <ul>
        {
          users.map((user) => (
            <li key={user.email}>
              { user.name.first + user.name.last }
            </li>
          ))
        }
      </ul>
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

這個範例程式碼我們會使用到 RandomUser 這個服務,這個服務在練習跟需要一些假的使用者資料時非常好用,完全不用擔心裡面有任何真人個資唷。

那麼 useMemo 除了與 Vue 的 Computed 神似之外,它還有什麼特性呢?首先 useMemo 其實可以傳入兩個參數,分別是 Callback 與 Array,而 useMemo 會去記憶 Callback 計算後的結果,也就是 memoized 的概念,甚至你也可以在第二個陣列中傳入想要監聽的變數,當監聽的變數有變化時就會出發渲染

const [ users, setUsers ] = React.useState([]);
const newUsers = React.useMemo(() => {}, [ users ])

但是如果你沒有傳入第二個陣列的話,則會在每一次觸發渲染時就執行一次 useMemo,如同前面範例一樣。

接下來讓我們實際撰寫一下會更有感覺,首先前面有提到 RandomUser 這個服務,在我們透過 AJAX 請求資料之後,我們會需要使用 sort 重新排序,所以你有可能這樣撰寫

const App = () => {
  const [ users, setUsers ] = React.useState([]);;
  const [ state, setState ] = React.useState(false);

  const getUsers = async() => {
    const { data } = await axios.get('https://randomuser.me/api/?results=10');
    setUsers(data.results);
  }

  React.useEffect(() => {
    getUsers();
  }, []);

  const filterUsers = (state) => {
    const newUser = users.sort((a, b) =>  {
      if(state) {
        return a.dob.age < b.dob.age ? 1 : -1
      }
      return a.dob.age > b.dob.age ? 1 : -1
    });
    
    setUsers([...newUser]);
  }

  return (
    <div>
      <button onClick={ () => filterUsers(true) } className="border-4 border-indigo-500">年齡大到小</button>
      <button onClick={ () => filterUsers(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
      <hr className="my-4"/>
      <ul>
        {
          users.map((user) =>(
            <li key={ user.email }>
              { `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
            </li>
          ))
        }
      </ul>
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

雖然畫面是有正常顯示且排序,但這樣做其實並不是一件好事,因為我們每次點擊按鈕都會觸發 filterUsers,而 filterUsers 會去重新排序,這樣就會造成效能上的浪費,因為我們只是想要重新排序,而不是重新渲染,所以我們可以使用 useMemo 來優化這個問題

這邊也題外話一下,你可能會注意到 filterUsers 中的 setUsers 我是重新做陣列展開的方式 setUsers([...newUser]);,這邊會這樣寫的原因是因爲 sort 會直接修改原本的陣列,因此如果你是直接 setUsers(newUser); 的話,你會發現畫面並不會重新渲染,因為我們並沒有修改資料,所以 React 在認知上就不會重新渲染,所以我們必須要做陣列展開的方式,讓 React 認知到我們有修改資料,才會重新渲染。

接著讓我們來看一下 useMemo 這個 Hook,那該怎麼寫,其實很簡單就把它當作在寫 Vue 的 computed 就好,只是我們會特別監聽 state 這個值有變化時才去觸發 useMemo

const App = () => {
  const [ users, setUsers ] = React.useState([]);;
  const [ state, setState ] = React.useState(false);

  const getUsers = async() => {
    const { data } = await axios.get('https://randomuser.me/api/?results=10');
    setUsers(data.results);
  }

  React.useEffect(() => {
    getUsers();
  }, []);

  const filterUsers = React.useMemo(() => {
    return users.sort((a, b) =>  {
      if(state) {
        return a.dob.age < b.dob.age ? 1 : -1
      }
      return a.dob.age > b.dob.age ? 1 : -1
    });
  }, [ state ])

  return (
    <div>
      <button onClick={ () => setState(true) } className="border-4 border-indigo-500">年齡大到小</button>
      <button onClick={ () => setState(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
      <hr className="my-4"/>
      <ul>
        {
          filterUsers.map((user, index) => (
            <li key={ user.email }>
              { `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
            </li>
          ))
        }
      </ul>
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

以上就是一個簡單粗略的 useMemo 介紹與使用。

後記

本文將會同步更新到我的部落格


上一篇
終究都要學 React 何不現在學呢? - React 進階 - 深入 JSX - (12)
下一篇
終究都要學 React 何不現在學呢? - React 進階 - useCallback - (14)
系列文
終究都要學 React 何不現在學呢?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言